文章目录
  1. 1. 第74条:谨慎地实现Serializable接口
  2. 2. 第75条:考虑使用自定义的序列化形式
  3. 3. 第76条:保护性的编写readObject方法
  4. 4. 第77条:对于实例控制,枚举类型优于readResolve
  5. 5. 第78条:考虑用序列化代理代替序列化实例
  6. 6. 总结

“将一个对象编码成一个字节流”,这个过程称为对象序列化,相反的过程称为反序列化。

第74条:谨慎地实现Serializable接口

只要添加implements Serializable,就可以使一个类可被序列化,写法上虽然看上去很简单,但是如果你认为真的这么简单的话就会付出很大的代价:

  • 一旦一个可序列化类被发布,就大大降低了“改变这个类的实现”的灵活性。
    1. 一旦一个类实现Serializable接口,它的字节流编码就变成了导出API的一部分,一旦这个类被广泛使用,往往必须永远支持这种序列化形式。
    2. 如果接受了默认的序列化形式,并且以后又要改变这个类的内部实现,结果可能会导致序列化形式不兼容
    3. 序列化会使类的演变受到限制,这种限制与序列化版本UID有关,每个可序列化的类都有一个唯一标识符serialVersionUID,如果没有指定,则系统会根据类的具体实现来进行自动生成,所以你修改了类,并且没有显示的指定UID,那么兼容性就会被破坏。
  • 它增加了Bug和安全漏洞的可能性
    序列化机制是一种语言之外的对象创建机制,所以反序列化是一个“隐藏的构造器”,具体与其他构造器相同的特点,所以反序列化过程也必须保证有构造器建立起来的约束关系,并且不允许攻击者在访问构造器过程中的内部对象,所以默认的反序列化机制很容易是对象约束遭到破坏,以及遭受非法的访问。
  • 随着类发行新的版本,相关测试的负担也增加了
    新的版本发布之后,要检查是否可以“在新版本中序列化一个实例,然后在旧版本中依然可以反序列化”,这个测试除了二进制兼容性以外,还要测试语义兼容性。所有测试的难度都是相当大啊!

下面是关于序列化类的操作:

  • 如果一个了类将要加入某个框架中,并且该框架依赖于序列化来实现对象传输或者持久化,那么实现Serializable这个接口就是非常有必要的。
  • 还有为了继承而设计的类尽量少的去实现Serializable接口,用户的接口也应该尽可能烧得继承Serializable接口,不然会对实现这些接口或者类的程序猿增加很多负担
  • 实现一个带有可序列化实例的类时,应该要注意类的约束条件,并且要实现

    1
    2
    3
    private void readObjectNoData() throws InvalidObjectException{
    throw new InvalidObjectException("stream data is required");
    }
  • 对于为继承而设计的不可序列化的类,你应该考虑提供一个无参构造器

  • 内部类不应该实现Serializable

简而言之,千万不要认为实现Serializable很容易,里面的坑多着呢。

第75条:考虑使用自定义的序列化形式

一个对象的默认序列化使将该对象进行物理表示,也就是说,默认序列化描述了该对象内部所包含的数据,以及每一个可以从这个对象到达其他对象的内部数据。

而对于一个对象来说,理想的序列化应该只包含该对象索比表示的逻辑数据,而逻辑数据和物理数据是应该相互独立的。

当对象的物理表示等同于它的路基表示是,使用默认的序列化是合理的:

1
2
3
4
5
public class Name implements Serializable{
private final String lastName;
private final String firstName;
private final String middelName;
}

类似这样的实体类,物理内容的这三个字段也可以很精确的反应它的逻辑内容。

来看看下面的序列化类:

1
2
3
4
5
6
7
8
9
10
public final class StringList implements Serializable{
private int size=0;
private Entry head = null;

private class Entry implements Serializable{
String data;
Entry next;
Entry previous;
}
}

从逻辑意义上讲,这个类表示一个字符串的序列,但是从物理意义上讲,把该序列化表示为一个双向链表。所以此时如果使用默认的序列化,那将会镜像除链表中所有的项以及这些项之间的所有双向链表。。-_-,大工程啊

当一个类的物理表示与它的逻辑内容有区别时,使用默认序列化将会有以下4个缺点:

  1. 它使这个类的导出API永远的束缚在该类的内容(StringList.Entry也会成为公有API的一部分)
  2. 会消耗很多的空间(因为会维护一个双向链表)
  3. 会消耗很多时间(遍历啊)
  4. 会引起栈溢出(递归啊)

其实类似这种类编写它的自定义序列化需求并不是很麻烦:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public final class StringList implements Serializable{
//添加“易变”标志防止默认序列化
private transient int size=0;
private transient Entry head = null;

private class Entry{
String data;
Entry next;
Entry previous;
}

public final void add(String s){
//TO DO
}

private void writeObject(ObjectOutputStream s) throws IOException
{

s.defaultWriteObject();
s.writeInt(size);
//在这里序列化
for(Entry e=head;e != null ; e=e.next)
s.writeObject(e.data);
}

private void readObject(ObjectInputStream s)
throws ClassNotFoundException,IOException
{

s.defaultReadObject();
//在这里进行反序列化
int numElements= s.readInt();
for(int i=0;i<numElements;i++)
add((String)s.readObject());
}
}

这里调用默认的readObject()writeObject()可以影响全类的序列化形式,极大的增强灵活性。

还有类似key-value的散列表也不适合使用默认的序列化,因为在不同的JVM中最终形成的散列位置可能会不一样。

切记不管使用什么序列化方式,建议都显示的加上序列的唯一版本UID

总而言之,当默认的序列化能合理的描述逻辑内容时,使用默认序列化就好了,否则建议使用自定义的序列化^_^

第76条:保护性的编写readObject方法

针对第39条的日期类,根据上一条的指导,貌似使用默认的序列化也是蛮合理的,增加implements Serializable即可。但是如果真的这么做,那么这个类将不再保证它的关键约束了。

因为readObject()方法相当于是一个接受字节流的构造函数,那如果有人伪造了这个字节流的话,反序列化出来的对象时相当危险的。还有一个信号就是反序列化出来对象还可以随意被改动,因为默认的序列化并没有使用保护性拷贝,所以如果增对该日期类实现自己的序列化的化,可以这么干:

1
2
3
4
5
6
7
8
9
10
11
private void readObject(ObjectInputStream s)
throws IOException,ClassNotFoundException
{

s.defaultReadObject();

start=new Date(start.getTime());//进行保护兴拷贝
end=new Date(end.getTime());

if(start.compareTo(end)>0)//进行安全性检查
throw new InvalidObjectException(start +"after"+ end);
}

下面是编写readObject方法的指导方针:

  • 对于对象引用域必须保持私有的类,要保护性得拷贝这个域中的每个对象。
  • 对于任何约束条件,如果检查失败,则应该抛出一个InvalidObjectException异常。
  • 如果整个对象图在被反序列化之后必须进行验证,就应该使用ObjectInputValidation接口
  • 无论是直接方式还是间接方法,都不要调用类中任何可被覆盖的方法

总而言之,当你编写readObject方法的时候,都要想:你正在编写一个公有的构造器,无论给他传递什么字节流,都必须产生一个有效的实例。

第77条:对于实例控制,枚举类型优于readResolve

一般的单例类,如果添加了implements Serializable之后,它就不再是一个单例,因为反序列化可以看做是另一个构造器,此时你就需要使用readResolve()方法,

1
2
3
4
private Object readResolve()
{

return INSTANCE;
}

直接返回这个单例就好了,但是注意的是这个单例需要用transient来标记

当然说了这么做,然而其实这并没什么卵用,这种方法还不如使用枚举单例 -_-,可以参考第3条,它写得简单,用的省心。

第78条:考虑用序列化代理代替序列化实例

序列化代理模式可以解决普通类实现序列化时带来的各种副作用。

序列化代理模式非常简单:

  1. 为可序列化类设计一个私有的静态嵌套类
  2. 为该嵌套类添加一个构造器,实现外围类的参数的复制
    3, 这个嵌套类中需要添加一个readResolve()方法进行外围类的返回
  3. 在外围类中添加writeReplace()方法进行外围类的复制
  4. 在外围类中还要添加一个readObject防止被攻击

根据上述指导,看下EnumSet源码中的序列化代理类的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* This class is used to serialize all EnumSet instances, regardless of
* implementation type. It captures their "logical contents" and they
* are reconstructed using public static factories. This is necessary
* to ensure that the existence of a particular implementation type is
* an implementation detail.
*
* @serial include
*/

private static class SerializationProxy <E extends Enum<E>>
implements java.io.Serializable
{

/**
* The element type of this enum set.
*
* @serial
*/

private final Class<E> elementType;

/**
* The elements contained in this enum set.
*
* @serial
*/

private final Enum[] elements;

SerializationProxy(EnumSet<E> set) {
elementType = set.elementType;//把数据拷贝进来
elements = set.toArray(ZERO_LENGTH_ENUM_ARRAY);
}

private Object readResolve() {
EnumSet<E> result = EnumSet.noneOf(elementType);
for (Enum e : elements)
result.add((E)e);
return result;//实例化Enumset
}

private static final long serialVersionUID = 362491234563181265L;
}

Object writeReplace() {//代理模式的入口
return new SerializationProxy<>(this);
}

// readObject method for the serialization proxy pattern
// See Effective Java, Second Ed., Item 78.
private void readObject(java.io.ObjectInputStream stream)
throws java.io.InvalidObjectException {//防止被攻击 竟然注释和Effective Java有关

throw new java.io.InvalidObjectException("Proxy required");
}

总而言之,推荐使用序列化代理模式!!!^_^

总结

真不容易,这本书终于看完了,这段时间因为工作日要上班,并且还要被老板催着科研论文,真心蛋疼~所以看书的进度还是比较慢啊。
不够回顾本书,里面还是提出了蛮多非常经典的建议的,感谢本书,感谢作者。^_^

文章目录
  1. 1. 第74条:谨慎地实现Serializable接口
  2. 2. 第75条:考虑使用自定义的序列化形式
  3. 3. 第76条:保护性的编写readObject方法
  4. 4. 第77条:对于实例控制,枚举类型优于readResolve
  5. 5. 第78条:考虑用序列化代理代替序列化实例
  6. 6. 总结